package healthcheck

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/romberli/go-multierror"
	"github.com/romberli/go-util/common"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/go-util/linux"
	"github.com/romberli/go-util/middleware/sql/statement"
	"github.com/romberli/log"
	"github.com/spf13/viper"

	"github.com/romberli/das/config"
	"github.com/romberli/das/global"
	"github.com/romberli/das/internal/app/sqladvisor"
	"github.com/romberli/das/internal/dependency/healthcheck"
	depquery "github.com/romberli/das/internal/dependency/query"
	"github.com/romberli/das/pkg/message"
	msghc "github.com/romberli/das/pkg/message/healthcheck"
	utilhc "github.com/romberli/das/pkg/util/healthcheck"
	util "github.com/romberli/das/pkg/util/query"
)

const (
	defaultMinScore                             = 0
	defaultMaxScore                             = 100.0
	defaultHundred                              = 100
	defaultDBConfigItemName                     = "db_config"
	defaultAvgBackupFailedRatioItemName         = "avg_backup_failed_ratio"
	defaultStatisticsFailedRatioItemName        = "statistics_failed_ratio"
	defaultCPUUsageItemName                     = "cpu_usage"
	defaultIOUtilItemName                       = "io_util"
	defaultDiskCapacityUsageItemName            = "disk_capacity_usage"
	defaultConnectionUsageItemName              = "connection_usage"
	defaultAverageActiveSessionPercentsItemName = "average_active_session_percents"
	defaultCacheMissRatioItemName               = "cache_miss_ratio"
	defaultTableRowsItemName                    = "table_rows"
	defaultTableSizeItemName                    = "table_size"
	defaultReplicationStatusItemName            = "replication_status"
	defaultSlowQueryRowsExaminedItemName        = "slow_query_rows_examined"

	defaultSlowQueryTopSQLNum = 3
)

var (
	_ healthcheck.Engine = (*DefaultEngine)(nil)

	ignoreDBList = []string{"information_schema", "performance_schema", "mysql", "test", "sys"}
)

// DefaultEngine work for health check module
type DefaultEngine struct {
	operationInfo        healthcheck.OperationInfo
	engineConfig         healthcheck.EngineConfig
	result               *Result
	mountPoints          []string
	devices              []string
	dasRepo              healthcheck.DASRepo
	applicationMySQLRepo healthcheck.ApplicationMySQLRepo
	prometheusRepo       healthcheck.PrometheusRepo
	queryRepo            healthcheck.QueryRepo
}

// NewDefaultEngine returns a new healthcheck.DefaultEngine
func NewDefaultEngine(operationInfo healthcheck.OperationInfo,
	dasRepo healthcheck.DASRepo,
	applicationMySQLRepo healthcheck.ApplicationMySQLRepo,
	prometheusRepo healthcheck.PrometheusRepo,
	queryRepo healthcheck.QueryRepo) healthcheck.Engine {
	return newDefaultEngine(operationInfo, dasRepo, applicationMySQLRepo, prometheusRepo, queryRepo)
}

// newDefaultEngine returns a new *DefaultEngine
func newDefaultEngine(operationInfo healthcheck.OperationInfo,
	dasRepo healthcheck.DASRepo,
	applicationMySQLRepo healthcheck.ApplicationMySQLRepo,
	prometheusRepo healthcheck.PrometheusRepo,
	queryRepo healthcheck.QueryRepo) *DefaultEngine {
	return &DefaultEngine{
		operationInfo:        operationInfo,
		engineConfig:         NewEmptyDefaultEngineConfig(),
		result:               NewEmptyResultWithOperationIDAndHostInfo(operationInfo.GetOperationID(), operationInfo.GetMySQLServerHostIP(), operationInfo.GetMySQLServerPortNum()),
		dasRepo:              dasRepo,
		applicationMySQLRepo: applicationMySQLRepo,
		prometheusRepo:       prometheusRepo,
		queryRepo:            queryRepo,
	}
}

// GetOperationInfo returns the operation information
func (de *DefaultEngine) GetOperationInfo() healthcheck.OperationInfo {
	return de.operationInfo
}

// getEngineConfig returns the default engine config
func (de *DefaultEngine) getEngineConfig() healthcheck.EngineConfig {
	return de.engineConfig
}

// getResult returns the result
func (de *DefaultEngine) getResult() healthcheck.Result {
	return de.result
}

// getMountPoints returns the mount points
func (de *DefaultEngine) getMountPoints() []string {
	return de.mountPoints
}

// getDevices returns the disk devices
func (de *DefaultEngine) getDevices() []string {
	return de.devices
}

// getDASRepo returns the das repository
func (de *DefaultEngine) getDASRepo() healthcheck.DASRepo {
	return de.dasRepo
}

// getApplicationMySQLRepo returns the application mysql repository
func (de *DefaultEngine) getApplicationMySQLRepo() healthcheck.ApplicationMySQLRepo {
	return de.applicationMySQLRepo
}

// getPrometheusRepo returns the prometheus repository
func (de *DefaultEngine) getPrometheusRepo() healthcheck.PrometheusRepo {
	return de.prometheusRepo
}

// getQueryRepo returns the query repository
func (de *DefaultEngine) getQueryRepo() healthcheck.QueryRepo {
	return de.queryRepo
}

// getItemConfig returns *DefaultItemConfig with given item name
func (de *DefaultEngine) getItemConfig(item string) healthcheck.ItemConfig {
	return de.engineConfig.GetItemConfig(item)
}

// Run runs healthcheck
func (de *DefaultEngine) Run() {
	defer func() {
		err := de.closeConnections()
		if err != nil {
			log.Errorf(constant.LogWithStackString, message.NewMessage(msghc.ErrHealthcheckCloseConnection, err))
		}
	}()

	// run
	err := de.run()
	if err != nil {
		log.Errorf(constant.LogWithStackString, message.NewMessage(msghc.ErrHealthcheckDefaultEngineRun, err))
		// update operation status
		updateErr := de.UpdateOperationStatusWithHTTP(defaultFailedStatus, err.Error())
		if updateErr != nil {
			log.Errorf(constant.LogWithStackString, message.NewMessage(msghc.ErrHealthcheckUpdateOperationStatus, updateErr, de.GetOperationInfo().GetOperationID()))
		}
		return
	}

	// update operation status
	msg := fmt.Sprintf("healthcheck completed successfully. operation_id: %d", de.GetOperationInfo().GetOperationID())
	updateErr := de.UpdateOperationStatusWithHTTP(defaultSuccessStatus, msg)
	if updateErr != nil {
		log.Errorf(constant.LogWithStackString, message.NewMessage(msghc.ErrHealthcheckUpdateOperationStatus, updateErr, de.GetOperationInfo().GetOperationID()))
	}
}

// run executes the healthcheck
func (de *DefaultEngine) run() error {
	// init MonitorRepo

	// pre run
	err := de.preRun()
	if err != nil {
		return err
	}
	// check db config
	err = de.checkDBConfig()
	if err != nil {
		return err
	}
	// check mysql average backup failed ratio
	err = de.CheckAvgBackupFailedRatio()
	if err != nil {
		return err
	}
	// check mysql statistics failed ratio
	err = de.CheckStatisticsFailedRatio()
	if err != nil {
		return err
	}
	// check cpu usage
	err = de.checkCPUUsage()
	if err != nil {
		return err
	}
	// check io util
	err = de.checkIOUtil()
	if err != nil {
		return err
	}
	// check disk capacity usage
	err = de.checkDiskCapacityUsage()
	if err != nil {
		return err
	}
	// check connection usage
	err = de.checkConnectionUsage()
	if err != nil {
		return err
	}
	// check average active session percents
	err = de.checkAverageActiveSessionPercents()
	if err != nil {
		return err
	}
	// check cache miss ratio
	err = de.checkCacheMissRatio()
	if err != nil {
		return err
	}
	// check table rows
	err = de.checkTableRows()
	if err != nil {
		return err
	}
	// check table size
	err = de.checkTableSize()
	if err != nil {
		return err
	}
	// check replication status
	err = de.checkReplicationStatus()
	if err != nil {
		return err
	}
	// check slow query
	err = de.checkSlowQuery()
	if err != nil {
		return err
	}
	// summarize
	de.summarize()
	// post run
	return de.postRun()
}

func (de *DefaultEngine) closeConnections() error {
	merr := &multierror.Error{}

	err := de.getApplicationMySQLRepo().Close()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	err = de.getQueryRepo().Close()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	return merr.ErrorOrNil()
}

// preRun performs pre-run actions
func (de *DefaultEngine) preRun() error {
	// load engine config
	err := de.loadEngineConfig()
	if err != nil {
		return err
	}
	// get file systems
	fileSystems, err := de.getPrometheusRepo().GetFileSystems()
	if err != nil {
		return err
	}
	// get total mount points
	var mountPoints []string
	for _, fileSystem := range fileSystems {
		mountPoints = append(mountPoints, fileSystem.GetMountPoint())
	}
	// get mysql directories
	dirs, err := de.getApplicationMySQLRepo().GetMySQLDirs()
	if err != nil {
		return err
	}

	dirs = append(dirs, constant.RootDir)

	// get mysql mount points and devices
	for _, dir := range dirs {
		mountPoint, err := linux.MatchMountPoint(dir, mountPoints)
		if err != nil {
			return err
		}

		if !common.ElementInSlice(de.mountPoints, mountPoint) {
			de.mountPoints = append(de.mountPoints, mountPoint)
		}

		for _, fileSystem := range fileSystems {
			device := fileSystem.GetDevice()
			if mountPoint == fileSystem.GetMountPoint() && !common.ElementInSlice(de.devices, device) {
				de.devices = append(de.devices, device)
			}
		}
	}

	return nil
}

// loadEngineConfig loads engine config
func (de *DefaultEngine) loadEngineConfig() error {
	var err error
	// load config
	de.engineConfig, err = de.getDASRepo().LoadEngineConfig()
	if err != nil {
		return err
	}
	// validate config
	return de.engineConfig.Validate()
}

// checkDBConfig checks database configuration
func (de *DefaultEngine) checkDBConfig() error {
	// load database config
	var configItems []string
	for item := range dbConfigVariableNames {
		configItems = append(configItems, item)
	}

	globalVariables, err := de.getApplicationMySQLRepo().GetVariables(configItems)
	if err != nil {
		return err
	}

	dbConfigConfig := de.getItemConfig(defaultDBConfigItemName)

	var (
		dbConfigCount int
		variables     []*Variable
	)

	for _, globalVariable := range globalVariables {
		name := globalVariable.GetName()
		value := globalVariable.GetValue()

		switch name {
		// max_connections
		case dbConfigMaxConnections:
			maxConnections, err := strconv.Atoi(value)
			if err != nil {
				return errors.Trace(err)
			}
			if maxConnections < dbConfigMaxConnectionsValid {
				dbConfigCount++
				variables = append(variables, NewVariable(dbConfigMaxConnections, value, strconv.Itoa(dbConfigMaxConnectionsValid)))
			}
		// slave_parallel_workers
		case dbConfigSlaveParallelWorkers:
			workers, err := strconv.Atoi(value)
			if err != nil {
				return errors.Trace(err)
			}
			if workers < dbConfigSlaveParallelWorkersValid {
				dbConfigCount++
				variables = append(variables, NewVariable(dbConfigSlaveParallelWorkers, value, strconv.Itoa(dbConfigSlaveParallelWorkersValid)))
			}
		// report_host
		case dbConfigReportHost:
			recommendValue := de.GetOperationInfo().GetMySQLServerHostIP()
			if value != recommendValue && value != de.GetOperationInfo().GetMySQLServerServerName() {
				dbConfigCount++
				variables = append(variables, NewVariable(dbConfigReportHost, value, recommendValue))
			}
		case dbConfigReportPort:
			recommendValue := strconv.Itoa(de.GetOperationInfo().GetMySQLServerPortNum())
			if value != recommendValue {
				dbConfigCount++
				variables = append(variables, NewVariable(dbConfigReportPort, value, recommendValue))
			}
		// others
		case dbConfigLogBin, dbConfigBinlogFormat, dbConfigBinlogRowImage, dbConfigSyncBinlog,
			dbConfigInnodbFlushLogAtTrxCommit, dbConfigGTIDMode, dbConfigEnforceGTIDConsistency,
			dbConfigSlaveParallelType, dbConfigMasterInfoRepository, dbConfigRelayLogInfoRepository,
			dbConfigInnodbFlushMethod, dbConfigInnodbMonitorEnable,
			dbConfigInnodbPrintAllDeadlocks, dbConfigSlowQueryLog, dbConfigPerformanceSchema:
			if strings.ToUpper(value) != dbConfigVariableNames[name] {
				dbConfigCount++
				variables = append(variables, NewVariable(name, value, dbConfigVariableNames[name]))
			}
		}
	}

	// database config data
	jsonBytesTotal, err := json.Marshal(globalVariables)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.DBConfigData = string(jsonBytesTotal)
	// database config advice
	jsonBytesVariables, err := json.Marshal(variables)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.DBConfigAdvice = string(jsonBytesVariables)
	// database config score deduction
	dbConfigScoreDeduction := float64(dbConfigCount) * dbConfigConfig.GetScoreDeductionPerUnitHigh()
	if dbConfigScoreDeduction > dbConfigConfig.GetMaxScoreDeductionHigh() {
		dbConfigScoreDeduction = dbConfigConfig.GetMaxScoreDeductionHigh()
	}

	de.result.DBConfigScore = int(defaultMaxScore - dbConfigScoreDeduction)

	if de.result.DBConfigScore < constant.ZeroInt {
		de.result.DBConfigScore = constant.ZeroInt
	}

	return nil
}

// CheckAvgBackupFailedRatio checks the mysql backup information
func (de *DefaultEngine) CheckAvgBackupFailedRatio() error {
	// get data
	datas, err := de.getPrometheusRepo().GetAvgBackupFailedRatio()
	if err != nil {
		return err
	}
	// parse data
	de.result.AvgBackupFailedRatioScore, de.result.AvgBackupFailedRatioData, de.result.AvgBackupFailedRatioHigh, err = de.parsePrometheusDatas(defaultAvgBackupFailedRatioItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// CheckStatisticsFailedRatio checks the statistic of mysql
func (de *DefaultEngine) CheckStatisticsFailedRatio() error {
	// get data
	datas, err := de.getPrometheusRepo().GetCPUUsage()
	if err != nil {
		return err
	}
	// parse data
	de.result.StatisticsFailedRatioScore, de.result.StatisticsFailedRatioData, de.result.StatisticsFailedRatioHigh, err = de.parsePrometheusDatas(defaultStatisticsFailedRatioItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkCPUUsage checks cpu usage
func (de *DefaultEngine) checkCPUUsage() error {
	// get data
	datas, err := de.getPrometheusRepo().GetCPUUsage()
	if err != nil {
		return err
	}
	// parse data
	de.result.CPUUsageScore, de.result.CPUUsageData, de.result.CPUUsageHigh, err = de.parsePrometheusDatas(defaultCPUUsageItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkIOUtil check io util
func (de *DefaultEngine) checkIOUtil() error {
	// get data
	datas, err := de.getPrometheusRepo().GetIOUtil()
	if err != nil {
		return err
	}
	// parse data
	de.result.IOUtilScore, de.result.IOUtilData, de.result.IOUtilHigh, err = de.parsePrometheusDatas(defaultIOUtilItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkDiskCapacityUsage checks disk capacity usage
func (de *DefaultEngine) checkDiskCapacityUsage() error {
	// get data
	multiLabelDatas, err := de.getPrometheusRepo().GetDiskCapacityUsage(de.getMountPoints())
	if err != nil {
		return err
	}
	// parse data
	de.result.DiskCapacityUsageScore, de.result.DiskCapacityUsageData, de.result.DiskCapacityUsageHigh, err = de.parseMultiLabelDatas(defaultDiskCapacityUsageItemName, multiLabelDatas)
	if err != nil {
		return err
	}

	return nil
}

// checkConnectionUsage checks connection usage
func (de *DefaultEngine) checkConnectionUsage() error {
	// get data
	datas, err := de.getPrometheusRepo().GetConnectionUsage()
	if err != nil {
		return err
	}
	// parse data
	de.result.ConnectionUsageScore, de.result.ConnectionUsageData, de.result.ConnectionUsageHigh, err = de.parsePrometheusDatas(defaultConnectionUsageItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkAverageActiveSessionPercents check active session number
func (de *DefaultEngine) checkAverageActiveSessionPercents() error {
	// get data
	datas, err := de.getPrometheusRepo().GetAverageActiveSessionPercents()
	if err != nil {
		return err
	}
	// parse data
	de.result.AverageActiveSessionPercentsScore, de.result.AverageActiveSessionPercentsData, de.result.AverageActiveSessionPercentsHigh, err = de.parsePrometheusDatas(defaultAverageActiveSessionPercentsItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkCacheMissRatio checks cache miss ratio
func (de *DefaultEngine) checkCacheMissRatio() error {
	// get data
	datas, err := de.getPrometheusRepo().GetCacheMissRatio()
	if err != nil {
		return err
	}
	// parse data
	de.result.CacheMissRatioScore, de.result.CacheMissRatioData, de.result.CacheMissRatioHigh, err = de.parsePrometheusDatas(defaultCacheMissRatioItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkTableSize checks table rows
func (de *DefaultEngine) checkTableRows() error {
	// get tables
	tables, err := de.getApplicationMySQLRepo().GetLargeTables()
	if err != nil {
		return err
	}

	tableRowsConfig := de.getItemConfig(defaultTableRowsItemName)

	var (
		tableRowsHighSum              int
		tableRowsHighCount            int
		tableRowsMediumSum            int
		tableRowsMediumCount          int
		tableRowsScoreDeductionHigh   float64
		tableRowsScoreDeductionMedium float64

		tableRowsHigh []healthcheck.Table
	)

	for _, table := range tables {
		switch {
		case float64(table.GetTableRows()) >= tableRowsConfig.GetHighWatermark():
			tableRowsHigh = append(tableRowsHigh, table)
			tableRowsHighSum += table.GetTableRows()
			tableRowsHighCount++
		case float64(table.GetTableRows()) >= tableRowsConfig.GetLowWatermark():
			tableRowsMediumSum += table.GetTableRows()
			tableRowsMediumCount++
		}
	}

	// table rows data
	jsonBytesTotal, err := json.Marshal(tables)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.TableRowsData = string(jsonBytesTotal)
	// table rows high
	jsonBytesHigh, err := json.Marshal(tableRowsHigh)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.TableRowsHigh = string(jsonBytesHigh)

	// table rows high score deduction
	if tableRowsHighCount > constant.ZeroInt {
		tableRowsScoreDeductionHigh = (float64(tableRowsHighSum)/float64(tableRowsHighCount) - tableRowsConfig.GetHighWatermark()) / tableRowsConfig.GetUnit() * tableRowsConfig.GetScoreDeductionPerUnitHigh()
		if tableRowsScoreDeductionHigh > tableRowsConfig.GetMaxScoreDeductionHigh() {
			tableRowsScoreDeductionHigh = tableRowsConfig.GetMaxScoreDeductionHigh()
		}
	}
	// table rows medium score deduction
	if tableRowsMediumCount > constant.ZeroInt {
		tableRowsScoreDeductionMedium = (float64(tableRowsMediumSum)/float64(tableRowsMediumCount) - tableRowsConfig.GetLowWatermark()) / tableRowsConfig.GetUnit() * tableRowsConfig.GetScoreDeductionPerUnitMedium()
		if tableRowsScoreDeductionMedium > tableRowsConfig.GetMaxScoreDeductionMedium() {
			tableRowsScoreDeductionMedium = tableRowsConfig.GetMaxScoreDeductionMedium()
		}
	}
	// table rows score
	de.result.TableRowsScore = int(defaultMaxScore - tableRowsScoreDeductionHigh - tableRowsScoreDeductionMedium)
	if de.result.TableRowsScore < constant.ZeroInt {
		de.result.TableRowsScore = constant.ZeroInt
	}

	return nil
}

// checkTableSize checks table sizes
func (de *DefaultEngine) checkTableSize() error {
	// get tables
	tables, err := de.getApplicationMySQLRepo().GetLargeTables()
	if err != nil {
		return err
	}

	tableSizeConfig := de.getItemConfig(defaultTableSizeItemName)

	var (
		tableSizeHighSum              float64
		tableSizeHighCount            int
		tableSizeMediumSum            float64
		tableSizeMediumCount          int
		tableSizeScoreDeductionHigh   float64
		tableSizeScoreDeductionMedium float64

		tableSizeHigh []healthcheck.Table
	)

	for _, table := range tables {
		switch {
		case table.GetTableSize() >= tableSizeConfig.GetHighWatermark():
			tableSizeHigh = append(tableSizeHigh, table)
			tableSizeHighSum += table.GetTableSize()
			tableSizeHighCount++
		case table.GetTableSize() >= tableSizeConfig.GetLowWatermark():
			tableSizeMediumSum += table.GetTableSize()
			tableSizeMediumCount++
		}
	}

	// table size data
	jsonBytesTotal, err := json.Marshal(tables)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.TableSizeData = string(jsonBytesTotal)
	// table size high
	jsonBytesHigh, err := json.Marshal(tableSizeHigh)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.TableSizeHigh = string(jsonBytesHigh)

	// table size high score deduction
	if tableSizeHighCount > constant.ZeroInt {
		tableSizeScoreDeductionHigh = (tableSizeHighSum/float64(tableSizeHighCount) - tableSizeConfig.GetHighWatermark()) / tableSizeConfig.GetUnit() * tableSizeConfig.GetScoreDeductionPerUnitHigh()
		if tableSizeScoreDeductionHigh > tableSizeConfig.GetMaxScoreDeductionHigh() {
			tableSizeScoreDeductionHigh = tableSizeConfig.GetMaxScoreDeductionHigh()
		}
	}
	// table size medium score deduction
	if tableSizeMediumCount > constant.ZeroInt {
		tableSizeScoreDeductionMedium = (tableSizeMediumSum/float64(tableSizeMediumCount) - tableSizeConfig.GetLowWatermark()) / tableSizeConfig.GetUnit() * tableSizeConfig.GetScoreDeductionPerUnitMedium()
		if tableSizeScoreDeductionMedium > tableSizeConfig.GetMaxScoreDeductionMedium() {
			tableSizeScoreDeductionMedium = tableSizeConfig.GetMaxScoreDeductionMedium()
		}
	}
	// table size score
	de.result.TableSizeScore = int(defaultMaxScore - tableSizeScoreDeductionHigh - tableSizeScoreDeductionMedium)
	if de.result.TableSizeScore < constant.ZeroInt {
		de.result.TableSizeScore = constant.ZeroInt
	}

	return nil
}

// checkReplicationStatus checks replication status
func (de *DefaultEngine) checkReplicationStatus() error {
	// check slave status
	slaveStatus, err := de.getApplicationMySQLRepo().GetSlaveStatus()
	if err != nil {
		return err
	}

	if !slaveStatus.IsSlave() {
		// this server is not a mysql slave
		de.result.ReplicationStatusScore = defaultMaxScore

		return nil
	}

	if slaveStatus.IsSlave() && !slaveStatus.IsRunning() {
		// this server is a mysql slave, but io thread and/or sql thread are not running.
		// in this case, will not check the replication lag anymore.
		de.result.ReplicationStatusScore = constant.ZeroInt

		return nil
	}

	// get data
	datas, err := de.getPrometheusRepo().GetReplicationLag()
	if err != nil {
		return err
	}
	// parse data
	de.result.ReplicationStatusScore, de.result.ReplicationStatusData, de.result.ReplicationStatusHigh, err = de.parsePrometheusDatas(defaultReplicationStatusItemName, datas)
	if err != nil {
		return err
	}

	return nil
}

// checkSlowQuery checks slow query
func (de *DefaultEngine) checkSlowQuery() error {
	// check slow query execution time
	slowQueries, err := de.getQueryRepo().GetSlowQuery()
	if err != nil {
		return err
	}

	var (
		i                                int
		slowQueryRowsExaminedHighSum     int
		slowQueryRowsExaminedHighCount   int
		slowQueryRowsExaminedMediumSum   int
		slowQueryRowsExaminedMediumCount int

		slowQueryRowsExaminedHighScore   float64
		slowQueryRowsExaminedMediumScore float64

		topSQLList []depquery.Query
	)

	slowQueryRowsExaminedConfig := de.getItemConfig(defaultSlowQueryRowsExaminedItemName)

	for _, slowQuery := range slowQueries {
		parseFailed := false
		if statement.GetType(slowQuery.GetExample()) == statement.Unknown {
			continue
		}
		if i < defaultSlowQueryTopSQLNum {
			dbName, err := util.GetDBName(slowQuery.GetExample())
			if err != nil {
				parseFailed = true
				log.Errorf("healthcheck defaultEngine.checkSlowQuery(): get db name of query failed, maybe the sql is too long and only part of the sql text was saved into slow query database. query_example: %s, error:\n%+v", slowQuery.GetExample(), err)
			}
			if !parseFailed && dbName == constant.EmptyString {
				tableNames, err := util.GetTableNames(slowQuery.GetExample())
				if err != nil {
					return err
				}
				dbName, err = de.getApplicationMySQLRepo().GetDBName(tableNames)
				if err != nil {
					return err
				}
			}
			if !common.ElementInSlice(ignoreDBList, dbName) {
				slowQuery.SetDBName(dbName)
				topSQLList = append(topSQLList, slowQuery)
				i++
			}
		}
		if slowQuery.GetRowsExaminedMax() >= int(slowQueryRowsExaminedConfig.GetHighWatermark()) {
			// slow query rows examined high
			slowQueryRowsExaminedHighSum += slowQuery.GetRowsExaminedMax()
			slowQueryRowsExaminedHighCount++
			continue
		}
		if slowQuery.GetRowsExaminedMax() >= int(slowQueryRowsExaminedConfig.GetLowWatermark()) {
			// slow query rows examined medium
			slowQueryRowsExaminedMediumSum += slowQuery.GetRowsExaminedMax()
			slowQueryRowsExaminedMediumCount++
		}
	}
	// slow query rows examined high score
	if slowQueryRowsExaminedHighCount > constant.ZeroInt {
		slowQueryRowsExaminedHighScore = (float64(slowQueryRowsExaminedHighSum)/float64(slowQueryRowsExaminedHighCount) - slowQueryRowsExaminedConfig.GetHighWatermark()) / slowQueryRowsExaminedConfig.GetUnit() * slowQueryRowsExaminedConfig.GetScoreDeductionPerUnitHigh()
		if slowQueryRowsExaminedHighScore > slowQueryRowsExaminedConfig.GetMaxScoreDeductionHigh() {
			slowQueryRowsExaminedHighScore = slowQueryRowsExaminedConfig.GetMaxScoreDeductionHigh()
		}
	}
	// slow query rows examined medium score
	if slowQueryRowsExaminedMediumCount > constant.ZeroInt {
		slowQueryRowsExaminedMediumScore = (float64(slowQueryRowsExaminedMediumSum)/float64(slowQueryRowsExaminedMediumCount) - slowQueryRowsExaminedConfig.GetLowWatermark()) / slowQueryRowsExaminedConfig.GetUnit() * slowQueryRowsExaminedConfig.GetScoreDeductionPerUnitMedium()
		if slowQueryRowsExaminedMediumScore > slowQueryRowsExaminedConfig.GetMaxScoreDeductionMedium() {
			slowQueryRowsExaminedMediumScore = slowQueryRowsExaminedConfig.GetMaxScoreDeductionMedium()
		}
	}
	// slow query score
	de.result.SlowQueryScore = int(defaultMaxScore - slowQueryRowsExaminedHighScore - slowQueryRowsExaminedMediumScore)
	if de.result.SlowQueryScore < defaultMinScore {
		de.result.SlowQueryScore = defaultMinScore
	}

	// sql tuning
	for _, sql := range topSQLList {
		var advice string

		// get db info
		if sql.GetDBName() != constant.EmptyString {
			// init sql advisor service
			advisorService := sqladvisor.NewServiceWithDefault()
			// get advice
			advice, err = advisorService.AdviseByHostInfoAndDBNameLocal(
				de.GetOperationInfo().GetMySQLServerHostIP(),
				de.GetOperationInfo().GetMySQLServerPortNum(),
				sql.GetDBName(),
				sql.GetExample(),
			)
			if err != nil {
				// TODO: if the tables that were used in the slow query do not exist anymore,
				//  or the sql is too long and only part of the sql text was saved into slow query table,
				//  and this will cause parsing error and advising error,
				//  therefore sql advisor returns an error, and we can do nothing here,
				//  so for now, log the error message and jump over this,
				//  we may optimize this later
				log.Error(message.NewMessage(msghc.ErrHealthcheckSQLAdvisorAdvice, err).Error())
				continue
			}
		} else {
			jsonBytes, err := json.Marshal(sql)
			if err != nil {
				return errors.Trace(err)
			}
			advice = string(jsonBytes)
		}

		de.result.SlowQueryAdvice += advice + constant.CommaString
	}

	// slow query data
	jsonBytesData, err := json.Marshal(topSQLList)
	if err != nil {
		return errors.Trace(err)
	}
	de.result.SlowQueryData = string(jsonBytesData)

	de.result.SlowQueryAdvice = constant.LeftBracketString + strings.Trim(de.result.SlowQueryAdvice, constant.CommaString) + constant.RightBracketString

	return nil
}

// summarize summarizes all item scores with weight
func (de *DefaultEngine) summarize() {
	de.result.WeightedAverageScore = (de.result.DBConfigScore*de.getItemConfig(defaultDBConfigItemName).GetItemWeight() +
		de.result.AvgBackupFailedRatioScore*de.getItemConfig(defaultAvgBackupFailedRatioItemName).GetItemWeight() +
		de.result.StatisticsFailedRatioScore*de.getItemConfig(defaultStatisticsFailedRatioItemName).GetItemWeight() +
		de.result.CPUUsageScore*de.getItemConfig(defaultCPUUsageItemName).GetItemWeight() +
		de.result.IOUtilScore*de.getItemConfig(defaultIOUtilItemName).GetItemWeight() +
		de.result.DiskCapacityUsageScore*de.getItemConfig(defaultDiskCapacityUsageItemName).GetItemWeight() +
		de.result.ConnectionUsageScore*de.getItemConfig(defaultConnectionUsageItemName).GetItemWeight() +
		de.result.AverageActiveSessionPercentsScore*de.getItemConfig(defaultAverageActiveSessionPercentsItemName).GetItemWeight() +
		de.result.CacheMissRatioScore*de.getItemConfig(defaultCacheMissRatioItemName).GetItemWeight() +
		de.result.TableRowsScore*de.getItemConfig(defaultTableRowsItemName).GetItemWeight() +
		de.result.TableSizeScore*de.getItemConfig(defaultTableSizeItemName).GetItemWeight() +
		de.result.SlowQueryScore*(de.getItemConfig(defaultSlowQueryRowsExaminedItemName).GetItemWeight())) /
		constant.MaxPercentage

	if de.result.WeightedAverageScore < defaultMinScore {
		de.result.WeightedAverageScore = defaultMinScore
	}
}

// postRun performs post-run actions, for now, it ony saves healthcheck result to the middleware
func (de *DefaultEngine) postRun() error {
	// save result
	err := de.SaveResultWithHTTP()
	if err != nil {
		return err
	}
	// TODO: remove the below code
	// send email
	// if de.getResult().GetWeightedAverageScore() < defaultMaxScore {
	// 	err = de.sendEmail()
	// if err != nil {
	// 	return err
	// }
	// }
	//
	return nil
}

// parsePrometheusDatas parses prometheus datas
func (de *DefaultEngine) parsePrometheusDatas(item string, datas []healthcheck.PrometheusData) (int, string, string, error) {
	cfg := de.getItemConfig(item)

	var (
		countTotal           int
		highSum              float64
		highCount            int
		mediumSum            float64
		mediumCount          int
		scoreDeductionHigh   float64
		scoreDeductionMedium float64

		highDatas []healthcheck.PrometheusData
	)
	// parse monitor data
	for _, data := range datas {
		countTotal++
		switch {
		case data.GetValue() >= cfg.GetHighWatermark():
			highDatas = append(highDatas, data)
			highSum += data.GetValue()
		case data.GetValue() >= cfg.GetLowWatermark():
			mediumSum += data.GetValue()
		}
	}

	// high score deduction
	if highCount > constant.ZeroInt {
		scoreDeductionHigh = (highSum/float64(highCount) - cfg.GetHighWatermark()) * float64(highCount) / float64(countTotal) / cfg.GetUnit() * cfg.GetScoreDeductionPerUnitHigh()
		if scoreDeductionHigh > cfg.GetMaxScoreDeductionHigh() {
			scoreDeductionHigh = cfg.GetMaxScoreDeductionHigh()
		}
	}
	// medium score deduction
	if mediumCount > constant.ZeroInt {
		scoreDeductionMedium = (mediumSum/float64(mediumCount) - cfg.GetLowWatermark()) * float64(mediumCount) / float64(countTotal) / cfg.GetUnit() * cfg.GetScoreDeductionPerUnitMedium()
		if scoreDeductionMedium > cfg.GetMaxScoreDeductionMedium() {
			scoreDeductionMedium = cfg.GetMaxScoreDeductionMedium()
		}
	}
	// calculate score
	score := int(defaultMaxScore - scoreDeductionHigh - scoreDeductionMedium)
	if score < constant.ZeroInt {
		score = constant.ZeroInt
	}

	jsonBytesTotal, err := json.Marshal(datas)
	if err != nil {
		return constant.ZeroInt, constant.EmptyString, constant.EmptyString, errors.Trace(err)
	}
	jsonBytesHigh, err := json.Marshal(highDatas)
	if err != nil {
		return constant.ZeroInt, constant.EmptyString, constant.EmptyString, errors.Trace(err)
	}

	return score, string(jsonBytesTotal), string(jsonBytesHigh), nil
}

// parsePrometheusDatas parses multi label datas
func (de *DefaultEngine) parseMultiLabelDatas(item string, multiDatas map[string][]healthcheck.PrometheusData) (int, string, string, error) {
	cfg := de.getItemConfig(item)

	var (
		score                int
		countTotal           int
		highSum              float64
		highCount            int
		mediumSum            float64
		mediumCount          int
		scoreDeductionHigh   float64
		scoreDeductionMedium float64
	)

	multiHighDatas := make(map[string][]healthcheck.PrometheusData)

	// parse monitor data

	for label, datas := range multiDatas {
		var highDatas []healthcheck.PrometheusData

		for _, data := range datas {
			countTotal++
			switch {
			case data.GetValue() >= cfg.GetHighWatermark():
				highDatas = append(highDatas, data)
				highSum += data.GetValue()
			case data.GetValue() >= cfg.GetLowWatermark():
				mediumSum += data.GetValue()
			}
		}

		multiHighDatas[label] = highDatas

		// high score deduction
		if highCount > constant.ZeroInt {
			scoreDeductionHigh = (highSum/float64(highCount) - cfg.GetHighWatermark()) * float64(highCount) / float64(countTotal) / cfg.GetUnit() * cfg.GetScoreDeductionPerUnitHigh()
			if scoreDeductionHigh > cfg.GetMaxScoreDeductionHigh() {
				scoreDeductionHigh = cfg.GetMaxScoreDeductionHigh()
			}
		}
		// medium score deduction
		if mediumCount > constant.ZeroInt {
			scoreDeductionMedium = (mediumSum/float64(mediumCount) - cfg.GetLowWatermark()) * float64(mediumCount) / float64(countTotal) / cfg.GetUnit() * cfg.GetScoreDeductionPerUnitMedium()
			if scoreDeductionMedium > cfg.GetMaxScoreDeductionMedium() {
				scoreDeductionMedium = cfg.GetMaxScoreDeductionMedium()
			}
		}
		// calculate score
		score = int(defaultMaxScore - scoreDeductionHigh - scoreDeductionMedium)
		if score < constant.ZeroInt {
			score = constant.ZeroInt
		}
	}

	jsonBytesTotal, err := json.Marshal(multiDatas)
	if err != nil {
		return constant.ZeroInt, constant.EmptyString, constant.EmptyString, errors.Trace(err)
	}
	jsonBytesHigh, err := json.Marshal(multiHighDatas)
	if err != nil {
		return constant.ZeroInt, constant.EmptyString, constant.EmptyString, errors.Trace(err)
	}

	return score, string(jsonBytesTotal), string(jsonBytesHigh), nil
}

// SaveResultWithHTTP saves the result with http
func (de *DefaultEngine) SaveResultWithHTTP() error {
	// set result id as same as the operation id
	de.result.ID = de.GetOperationInfo().GetOperationID()
	// set create time and last update time
	de.result.CreateTime = time.Now()
	de.result.LastUpdateTime = time.Now()

	// save the result to the middleware
	err := de.getDASRepo().SaveResultLocal(de.result)
	if err != nil {
		return err
	}

	callbackAddr := de.GetOperationInfo().GetCallbackAddr()
	if viper.GetString(config.RoleKey) == config.AgentRole &&
		callbackAddr != constant.EmptyString &&
		callbackAddr != viper.GetString(config.ServerCallbackAddrKey) {
		// save the result to the callback address
		resultBytes, err := de.getResult().Marshal()
		if err != nil {
			return errors.Trace(err)
		}
		// get special token
		token, err := de.getSpecialToken()
		if err != nil {
			return err
		}
		// prepare request body
		tmp := &struct {
			Token     string `json:"token"`
			Result    string `json:"result"`
			LoginName string `json:"login_name"`
		}{
			Token:     token,
			Result:    string(resultBytes),
			LoginName: de.GetOperationInfo().GetLoginName(),
		}
		data, err := json.Marshal(tmp)
		if err != nil {
			return errors.Trace(err)
		}
		log.Debugf("healthcheck DefaultEngine.SaveResultWithHTTP(): data: %s", string(data))
		// send request
		respBody, err := global.HTTPClient.PostDAS(de.GetOperationInfo().GetCallbackAddr()+utilhc.SaveResultPath, data)
		if err != nil {
			return err
		}
		log.Debugf("healthcheck DefaultEngine.SaveResultWithHTTP(): respBody: %s", respBody)
	}

	return nil
}

// UpdateOperationStatusWithHTTP updates the operation status with http
func (de *DefaultEngine) UpdateOperationStatusWithHTTP(status int, message string) error {
	// update operation status
	err := de.getDASRepo().UpdateOperationStatusLocal(de.GetOperationInfo().GetOperationID(), status, message)
	if err != nil {
		return err
	}

	callbackAddr := de.GetOperationInfo().GetCallbackAddr()
	if viper.GetString(config.RoleKey) == config.AgentRole &&
		callbackAddr != constant.EmptyString &&
		callbackAddr != viper.GetString(config.ServerCallbackAddrKey) {
		// update operation status to the callback address
		// get special token
		token, err := de.getSpecialToken()
		if err != nil {
			return err
		}
		// prepare request body
		data := []byte(fmt.Sprintf(utilhc.UpdateOperationStatusHTTPBodyTemplate,
			token, de.GetOperationInfo().GetOperationID(), status, message, de.GetOperationInfo().GetLoginName()))
		log.Debugf("healthcheck DefaultEngine.UpdateOperationStatusWithHTTP(): data: %s", string(data))
		// send request
		respBody, err := global.HTTPClient.PostDAS(de.GetOperationInfo().GetCallbackAddr()+utilhc.UpdateOperationStatusPath, data)
		if err != nil {
			return err
		}
		log.Debugf("healthcheck DefaultEngine.UpdateOperationStatusWithHTTP(): respBody: %s", respBody)
	}

	return nil
}

func (de *DefaultEngine) getSpecialToken() (string, error) {
	tokens := viper.GetStringSlice(config.PrivilegeSpecialTokensKey)
	if len(tokens) == constant.ZeroInt {
		return constant.EmptyString, errors.New("healthcheck DefaultEngine.getSpecialToken(): no special token found")
	}

	return tokens[constant.ZeroInt], nil
}

// TODO: remove the below code
// func (de *DefaultEngine) sendEmail() error {
// 	result, err := de.getDASRepo().GetResultByOperationID(de.GetOperationInfo().GetOperationID())
// 	if err != nil {
// 		return err
// 	}
//
// 	cfg := alert.NewConfigFromFile()
// 	cfg.Set(httpSystemNameJSON, de.GetOperationInfo().GetAppName())
// 	cfg.Set(httpHostIPJSON, de.GetOperationInfo().GetMySQLServer().GetHostIP())
// 	cfg.Set(httpEventIDJSON, strconv.Itoa(de.GetOperationInfo().GetOperationID()))
// 	cfg.Set(httpAlertTimeJSON, time.Now().Format(constant.TimeLayoutSecond))
// 	alertService := alert.NewServiceWithDefault(cfg)
//
// 	return alertService.SendEmail(
// 		de.GetOperationInfo().GetUser().GetEmail(),
// 		constant.EmptyString,
// 		fmt.Sprintf(defaultAlertSubjectTemplate, de.GetOperationInfo().GetAppName()),
// 		result.String(),
// 	)
// }
